#define _CRT_SECURE_NO_WARNINGS 1
#include"snake.h"


//记录
int scoremax = 0;

// 清空控制台输入缓冲区
void ClearConsoleInputBuffer() 
{
	HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE);
	FlushConsoleInputBuffer(hInput); // 直接清空 Windows 控制台输入缓冲区
}

//保存记录到文件函数
void datasave()
{
	FILE* fp = fopen("data.txt", "w");
	assert(fp != nullptr);
	fprintf(fp, "%d", scoremax);
	fclose(fp);
}

//从文件中读取记录
void dataload()
{
	FILE* fp = fopen("data.txt", "r");
	assert(fp != nullptr);
	fscanf(fp, "%d", &scoremax);
	fclose(fp);
}


//设置光标位置函数
void SetPos(short x, short y)
{
	//获取标准输入的句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//设置光标位置：COORD坐标类型
	COORD pos = { x,y };
	SetConsoleCursorPosition(houtput, pos);
}


//开始界面的打印和操作教程的打印
void WelcomeToGame()
{
	//需要先设置一下光标的位置
	SetPos(37,13);
	//打印开始信息
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	SetPos(39, 20);
	system("pause");

	//清空一下屏幕，再设置一下坐标打印操作教程信息
	system("cls"); 
	SetPos(23, 13);
	wprintf(L"用W . S . A . D 分别控制蛇的移动，按F3加速，按F4减速\n");
	SetPos(37, 15);
	wprintf(L"加速能够得到更高的分数！\n");

	SetPos(36, 24);
	system("pause");

	system("cls"); //再清理一下屏幕，这个开始界面就打印完了
}


//绘制地图 
void CreateMap()
{
	//绘制一个用□表示边界的27行，58列的棋盘
	//也就是在x轴方面，因为□为宽字符，一次占两格，我们只需要连着打印29个□就行
	//上方
	for (int i = 0;i < 29;i++)
	{
		wprintf(L"%lc", L'□');
	}

	//下方
	SetPos(0, 26);
	for (int i = 0;i < 29;i++)
	{
		wprintf(L"%lc", L'□');
	}

	//然后是左右方，那就是补上连接上下空缺的位置就好了,也就是竖着缺25个□
	//然后设置光标的话通过循环内部让光标每次向下走一格
	
	//左方
	for (int i = 1;i <= 25;i++)
	{
		SetPos(0, i);
		wprintf(L"%lc\n", L'□');
	}

	//右方
	for (int i = 1;i <= 25;i++)
	{
		SetPos(56, i);
		wprintf(L"%lc\n", L'□');
	}

}


//创建蛇身
void InitSnake(pSnake ps)
{
	pSnakdeNode cur = NULL;
	//假设一开始蛇有5个节点
	for (int i = 0;i < 5;i++)
	{
		//开辟节点空间
		cur = new SnakeNode;
		assert(cur!=NULL);

		cur->_next = NULL;
		//设置坐标时为了方便后续更改，可以设置成宏再传入宏
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;

		//采用头插法连接节点
		if (ps->_pSnake==NULL) //空链表
		{
			//那么这个节点就是头
			ps->_pSnake = cur;
		}
		else //非空
		{
			//让新的节点的next指向原来的头
			cur->_next = ps->_pSnake;
			//将头指向这个新的节点表示为新的头
			ps->_pSnake = cur;
		}
	}

	//遍历打印蛇身
	cur = ps->_pSnake;
	while (cur)
	{
		//得先设置一下要打印得位置（也就是光标）
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'●'); //打印蛇身
		cur = cur->_next; 
	}

	//设置贪吃蛇的属性
	ps->_dir = RIGHT; //默认向右走
	ps->_food_weight = 10; //一个食物一开始所得10分
	ps->_score = 0; //初始总分0
	ps->_sleep_time = 200; //单位是毫秒，初始每次休息200毫秒
	ps->_status = OK; //初始蛇状态
}


//创建食物
void CreateFood(pSnake ps)
{
	//随机生成一个在棋盘中的坐标，并且不能和蛇身重叠
	//并且食物的x坐标必须是偶数这样蛇才能吃到
	//所以x的范围为2-54，y的范围为1-25
	//x让它%53+2就可以得到在2-54范围内的数了
	//y让它%24+1就可以得到在1-25范围内的数了
	//再给个do while循环让x为2的倍数的随机数的时候才停下来
again:
	int x = 0, y = 0;
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 24 + 1;
	} while (x % 2 != 0);
	SetPos(x, y);

	//要让食物坐标不和蛇冲突,得再遍历一遍蛇节点坐标，然后判断
	pSnakdeNode cur = ps->_pSnake;
	while (cur)
	{
		if (x == cur->x && y == cur->y)
		{
			//说明重叠，需要重新生成，我们这里采用goto again得方式让执行回到生成随机数那里
			goto again;
		}
		cur = cur->_next;
	}

	//到这里食物的坐标合法，创建食物节点
	pSnakdeNode pfood = new SnakeNode;
	assert(pfood != NULL);
	pfood->x = x, pfood->y = y, pfood->_next = NULL;
	//打印食物
	SetPos(x, y); //定位位置
	wprintf(L"%lc", L'★');

	//将食物节点信息记录到蛇信息中
	ps->_pfood = pfood;
}



//初始化函数
void GameStart(pSnake ps)
{
	//0.先设置窗口大小，再隐藏光标（不然界面上光标会一直闪，不好看）
	//ok,到这开始就需要用到wind32相关函数了,记得包含头文件哦
	//设置控制台窗口的大小：30行，100列
	system("mode con cols=100 lines=30");
	//设置cmd窗口名称
	system("title 贪吃蛇");

	//获取标准输出的句柄(用来标识不同设备的数值)
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//隐藏光标操作
	CONSOLE_CURSOR_INFO CursorInfo; //定义一个光标信息的结构体
	GetConsoleCursorInfo(houtput, &CursorInfo); //获取控制台光标信息
	CursorInfo.bVisible = false; //隐藏控制台光标
	//调用SetConsoleCursorInfo函数，就会将这个隐藏光标的设置应用到对应的控制台上
	SetConsoleCursorInfo(houtput, &CursorInfo);


	//1.打印开始界面
	//2.打印功能介绍（按键教程）
	WelcomeToGame();

	//3.绘制地图
	CreateMap();

	//4.创建蛇
	//6.设置游戏相关信息
	InitSnake(ps);

	//5.创建食物
	CreateFood(ps); //食物在哪个位置要记录在蛇的类中，所以得传ps进去
}

void PrintfOperator()
{
	SetPos(60, 12);
	wprintf(L"%ls", L"不能穿墙，不能撞到自己\n");
	SetPos(60, 13);
	wprintf(L"%ls", L"用W . S . A . D 分别控制蛇的移动\n");
	SetPos(60, 14);
	wprintf(L"%ls", L"按F3加速，按F4减速\n");
	SetPos(60, 16);
	wprintf(L"%ls", L"按esc正常退出，按空格暂停游戏\n");
}



//可以把判断按键有没有按下封装成一个宏函数
//结果为1表示按过，为0表示未按过
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)

//暂停函数
void Pause(pSnake ps)
{
	while (1)
	{
		if (KEY_PRESS(0x20)) //再次按下空格键表示继续
		{
			break;
		}

		if (KEY_PRESS(0x1B))
		{
			ps->_status = END_NORMAL;
			break;
		}

		Sleep(200);
	}
}

//判断蛇下一个坐标是否是食物
bool NextIsFood(pSnakdeNode pnext, pSnake ps)
{
	if (pnext->x == ps->_pfood->x && pnext->y == ps->_pfood->y)
	{
		return true;
	}
	else
	{
		return false;
	}
}

//下一个位置是食物就吃掉
void EatFood(pSnakdeNode pnext, pSnake ps)
{
	//头插法，这个食物的节点就是我们蛇新的头结点
	ps->_pfood->_next = ps->_pSnake;
	ps->_pSnake = ps->_pfood;

	//释放原先本来作为下一个节点的节点空间
	delete pnext;
	pnext = nullptr;

	//再接着打印增长的蛇身
	pSnakdeNode cur = ps->_pfood;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'●');
		cur = cur->_next;
	}

	//吃掉食物后总分要增加
	ps->_score += ps->_food_weight;

	//再重新创建食物
	CreateFood(ps);
}


//下一个位置不是食物就头插这个节点，然后处理尾节点
void NoFood(pSnakdeNode pnext, pSnake ps)
{
	//头插新节点
	pnext->_next = ps->_pSnake;
	ps->_pSnake = pnext;

	//重新打印蛇身以及处理尾节点
	//1.把最后这个尾节点位置打印成空白字符（2个空格）
	//2.释放尾节点
	//3.尾节点的前一个节点的指针需要置空（防止野指针）
	pSnakdeNode cur = pnext;
	while (cur->_next->_next) //所以让cur找倒数第二个节点就好了
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'●');
		cur = cur->_next;
	}
	//此时cur->_next指向的就是尾节点
	//定位该坐标
	SetPos(cur->_next->x, cur->_next->y);
	printf("  ");
	delete cur->_next;
	cur->_next = nullptr;
}


//检测蛇是否撞墙
void KillByWall(pSnake ps)
{
	if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || 
		ps->_pSnake->y == 0|| ps->_pSnake->y == 26)
	{
		ps->_status = KILL_BY_WALL;
	}
}

//检测蛇是否撞到自己
void KillBySelf(pSnake ps)
{
	pSnakdeNode cur = ps->_pSnake->_next;
	while (cur)
	{
		if (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y)
		{
			ps->_status = KILL_BY_SELF;
			break;
		}
		cur = cur->_next;
	}
}


//蛇走一步的过程
void SnakeMove(pSnake ps)
{
	//创建一个蛇节点，表示蛇头即将到的下一个节点
	pSnakdeNode _pNextNode = new SnakeNode;
	assert(_pNextNode != NULL);

	//现需要知道蛇要走的方向，才好通过头节点确定下一个蛇节点的位置
	//蛇头坐标如果是x,y
	switch (ps->_dir)
	{
	case UP:
		//那么下一个节点坐标x,y-1
		_pNextNode->x = ps->_pSnake->x;
		_pNextNode->y = ps->_pSnake->y - 1;
		break;

	case DOWN:
		//那么下一个节点坐标x,y+1
		_pNextNode->x = ps->_pSnake->x;
		_pNextNode->y = ps->_pSnake->y + 1;
		break;

	case LEFT:
		//那么下一个节点坐标x-2,y,因为前面说过的蛇节点x坐标必须是偶数
		_pNextNode->x = ps->_pSnake->x - 2;
		_pNextNode->y = ps->_pSnake->y;
		break;

	case RIGHT:
		//那么下一个节点坐标x+2,y
		_pNextNode->x = ps->_pSnake->x + 2;
		_pNextNode->y = ps->_pSnake->y;
		break;
	}

	//不要忘记食物是不可以和蛇节点坐标重叠的，所以我们接下来还要判断一下他们坐标是否一样
	if (NextIsFood(_pNextNode,ps))
	{
		//是食物就吃掉
		EatFood(_pNextNode, ps);
	}
	else
	{
		//如果下一个坐标不是食物
		NoFood(_pNextNode, ps);
	}

	//检测蛇是否撞墙
	KillByWall(ps);

	//检测蛇是否撞到自己
	KillBySelf(ps);

}


//游戏运行的逻辑
void GameRun(pSnake ps)
{
	//打印操作教程信息
	PrintfOperator();

	//游戏主逻辑
	do
	{
		//进来先设置位置打印一下分数信息
		SetPos(60, 7);
		printf("最高分记录：%d", scoremax);
		SetPos(60, 8);
		printf("总得分：%d", ps->_score);
		SetPos(60, 9);
		printf("当前食物分数：%2d", ps->_food_weight);

		//检测按键，用一个宏函数
		//检测W键表示操作蛇方向向上，但是不能一开始向下时让蛇往上移动
		if (KEY_PRESS(0x57) && ps->_dir != DOWN) 
		{
			ps->_dir = UP;
		}
		//检测S键表示操作蛇方向向下，但是不能一开始向上时让蛇往下移动
		else if (KEY_PRESS(0x53) && ps->_dir != UP)
		{
			ps->_dir = DOWN;
		}
		//检测A键表示操作蛇方向向左，但是不能一开始向右时让蛇往左移动
		else if (KEY_PRESS(0x41) && ps->_dir != RIGHT)
		{
			ps->_dir = LEFT;
		}
		//检测D键表示操作蛇方向向右，但是不能一开始向左时让蛇往右移动
		else if (KEY_PRESS(0x44) && ps->_dir != LEFT)
		{
			ps->_dir = RIGHT;
		}
		else if (KEY_PRESS(0x1B)) //按esc键
		{
			//表示游戏正常结束
			ps->_status = END_NORMAL;
		}
		else if (KEY_PRESS(0x20)) //按空格键
		{
			//表示游戏暂停，死循环睡眠就行了
			//再次按空格就恢复break
			//我们封装成一个函数
			Pause(ps);
		}
		else if (KEY_PRESS(0x72)) //按F3
		{
			//表示加速,最多加速4档,每档-30休息时间
			if (ps->_sleep_time > 80)
			{
				ps->_sleep_time -= 30;
				ps->_food_weight += 2;
			}
		}
		else if (KEY_PRESS(0x73)) //按F4
		{
			//表示减速,也是和加速一样四个挡位
			if (ps->_food_weight>2) //这里用食物分数来处理
			{
				ps->_sleep_time += 30;
				ps->_food_weight -= 2;
			}
		}

		//蛇移动的每一步都得判断一下蛇的状态
		//走一步休息一下
		SnakeMove(ps); //蛇走一步的过程，也可能会影响蛇的状态，得传蛇对象地址
		Sleep(ps->_sleep_time);


	} while (ps->_status == OK);
}



//游戏结束
void GameEnd(pSnake ps)
{
	//通过游戏状态检测一下游戏结束的原因
	//设置一下位置以便打印结束信息
	SetPos(15, 12);
	switch (ps->_status)
	{
	case END_NORMAL:
		printf("您主动结束游戏!\n");
		break;

	case KILL_BY_WALL:
		printf("您因为撞到墙上游戏结束，太不小心了吧qwq\n");
		break;

	case KILL_BY_SELF:
		printf("您吃到了自己导致游戏结束\n");
		break;

	}
	SetPos(15, 13);
	printf("你最终得分是：%d\n", ps->_score);
	if (scoremax < ps->_score)
	{
		SetPos(15, 14);
		printf("！！您超过记录！！\n");
		scoremax = max(scoremax, ps->_score);
	}

	
	//释放链表
	pSnakdeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakdeNode prev = cur;
		cur = cur->_next;
		delete prev;
		prev = nullptr;
	}

}